Skip to content

Backport #4227 to 4.x: Let a reused engine discard the host objects its warmed caches hold - #4229

Merged
lahma merged 1 commit into
sebastienros:4.xfrom
lahma:backport-4227-4x
Oct 6, 2026
Merged

lahma merged 1 commit into
sebastienros:4.xfrom
lahma:backport-4227-4x

Conversation

@lahma

@lahma lahma commented Oct 5, 2026

Copy link
Copy Markdown
Collaborator

Backport of #4227 (fa0ee1354) to 4.x. Merge this only after #4227 has merged on main.

What it adds

Engine.Advanced.DiscardInterpreterCaches() is a new opt-in call for hosts that reuse an engine through CaptureGlobalSnapshot and RestoreGlobalSnapshot. A warmed call site keeps its last callee, and a warmed member-read site keeps its last receiver. On a pooled engine, that kept a finished request's host objects alive. One example is a delegate closing over the request's whole IServiceProvider, which is the Orchard Core engine-pool case that stays on 4.x.

The call drops the engine-owned handler trees (_functionDefinitions, _scriptStatementLists). It also releases the body tree of every definition it can reach:

  • the cached definitions
  • the realm's new Function cache, including the parked _dynamicCachedEnv
  • functions bound on the global object or in global lexical bindings, including their _envReuse

Finally, it empties the ReferencePool. Two caches stay on purpose: _evaluatedScripts, so the next run re-caches at once, and _propertyKeyExpressions, which needs handler identity for #3142. The call refuses while an evaluation is in progress, the same refusal RestoreGlobalSnapshot makes. The design rationale and the list of what it does not reach are in #4227.

The only evaluation-path change is in JintFunctionDefinition.EvaluateConciseBodyAsync (async expression-bodied arrow). Its resume delegate now captures the body handler it started on instead of this, so the closure is the same size.

This is additive public API, so the 4.x release that carries it should be a minor version (4.17.0), not a patch.

4.x adaptations

  • The public entry has no ThrowIfRetired or EnterHostCall, because neither exists on 4.x. Its guard is exactly 4.x's RestoreGlobalSnapshot guard (IsEvaluationInProgress || HasPendingAsyncOperations).
  • 4.x has no EngineMemoryReport/HandlerTreeCacheReport. That XML-doc change and the main-only cache-count properties and ObjectPool.PooledCount that came along as context were dropped. ObjectPool.Clear() and ReferencePool.Clear() are ported as they are.
  • Tests are converted to xUnit v3: [Theory]/[InlineData], and a DisableParallelization collection in place of [NonParallelizable]. Tasks.* becomes Advanced.RegisterPromise/ProcessTasks. The cache counts are read by reflection, as the existing GlobalSnapshotInternalsTests does, instead of through the memory report.
  • Docs: 4.x has no docs/guide/performance.md or interpreter AGENTS.md. The pooling paragraph went into the README's "Reusing a configured engine" section. The gotcha edits went into the root AGENTS.md, minus the HandlerTreeCacheReport reference. The RestoreGlobalSnapshot and ResetCallStack XML docs are ported as they are.
  • Public API baselines are updated for net10.0, net8.0, net462, netstandard2.0 and netstandard2.1, one line each.

Evidence

Unfixed 4.x. For this run, the product change was reverted and replaced by a no-op DiscardInterpreterCaches() stub so that the tests compile.

  • InterpreterCacheDiscardTests: 8 of 16 fail on net10.0 and 8 of 16 fail on net472. That is every discard: true retention case (6), the re-cache test and the refusal test. All 6 no-discard controls pass, which shows the retention is real on 4.x.
  • HostInterpreterCacheDiscardTests: 2 of 2 fail on each TFM.

With the fix, all of the above pass on both TFMs. As an ablation, I reverted only the concise-arrow capture: ASuspendedAsyncArrowResumesAcrossADiscard then fails with a NullReferenceException.

Full suites on this branch, Release:

Project net10.0 net472
Jint.Tests 7804 passed, 4 skipped, 0 failed 7719 passed, 4 skipped, 0 failed
Jint.Tests.PublicInterface (incl. API baselines) 1949 passed, 9 skipped, 0 failed 1941 passed, 9 skipped, 0 failed

No benchmarks were run. As noted on #4227, only the async concise-arrow path changed.

🤖 Generated with Claude Code

…st objects its warmed caches hold

A host that reuses an engine through CaptureGlobalSnapshot/RestoreGlobalSnapshot
keeps the warmed handler trees on purpose, but a warmed call site keeps its last
callee and a member-read site its last receiver, and nothing cleared them. A
global method built per request from an IServiceProvider therefore kept a
finished request's services alive on an idle pooled engine.

Engine.Advanced.DiscardInterpreterCaches() is the opt-in remedy. It drops the
engine-owned handler trees (_functionDefinitions, _scriptStatementLists),
releases the body tree of every definition it can reach - the cached ones, the
realm's new Function cache, and functions bound on the global surface (with
their parked call environment) - and empties the ReferencePool.
_evaluatedScripts stays so the next run re-caches at once, and
_propertyKeyExpressions stays because a suspended computed key finds its parked
state by handler identity.

The async concise-arrow resume delegate now captures the body handler it
started on instead of re-reading the field, in place of capturing `this`, so
the closure is no larger. No other evaluation path changed.

4.x adaptations: no ThrowIfRetired/EnterHostCall guard (neither exists on 4.x;
the refusal matches 4.x's RestoreGlobalSnapshot), no EngineMemoryReport doc
change (no memory report on 4.x), tests in xUnit v3 reading the caches by
reflection as GlobalSnapshotInternalsTests does, docs in README.md and the
root AGENTS.md instead of docs/guide/performance.md and the interpreter
AGENTS.md, and API baselines include net462.

(cherry picked from commit fa0ee13)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant